/**
* +--------------------------------------+
* | douban.fm                192kbps PRO |
* |--------------------------------------|
* |                                      |
* | ++++++++++++++++++++++++++++++++++++ |
* | ++++++++++++++++++++++++++++++++++++ |
* | ++++++++++++++++++++++++++++++++++++ |
* | ++++++++++++++++++++++++++++++++++++ |
* | ++++++++++++++++++++++++++++++++++++ |
* |                                      |
* |                          +---+ +-+-+ |
* | A tiny and smart cli     |+++| | + | |
* | player based on Node.js  +---+ +-+-+ |
* |                                      |
* +--------------------------------------+
*
* Douban.fm command line interface based on Node.js
* @author: 新浪微博@郭宇 [turingou](http://guoyu.me)
*
**/

import path from 'path'
import open from 'open'
import home from 'home'
import fsplus from 'fsplus'
import mkdirp from 'mkdirp'
import Player from 'player'
import color from 'colorful'
import douban from 'douban-sdk'
import Promise from 'bluebird'
import termList from 'term-list-enhanced'

import utils from './utils'
import pkg from '../package'
import errors from './errors'
import template from './template'

// Keypress shortcut list
const shorthands = {
  'return': 'play',
  'backspace': 'stop',
  'g': 'go',
  'l': 'loving',
  'n': 'next',
  'q': 'quit',
  's': 'share',
  'r': 'showLrc'
}

const localMhz = {
  'localMhz': {
    'seq_id': -99,
    'abbr_en': 'localMhz',
    'name': '本地电台',
    'channel_id': -99,
    'name_en': 'localMhz'
  },
  'privateMhz': {
    'seq_id': -3,
    'abbr_en': '',
    'name': '红心兆赫',
    'channel_id': -3,
    'name_en': '',
  },
}

const fs = Promise.promisifyAll(fsplus)
const mkdir = Promise.promisifyAll(mkdirp)

// Class Douban.fm
export default class FM {
  constructor() {
    // Fetch user's home
    this.USERHOME = home()

    // Resolve config files' path
    this.path = {}
    this.path.profile = home.resolve('~/.douban.fm.profile.json')
    this.path.history = home.resolve('~/.douban.fm.history.json')

    // Read configs from JSON files
    this.profile = utils.readJSON(this.path.profile)

    // Get music download folder as `this.home`
    this.home = this.profile.home || home.resolve('~/douban.fm')

    // Get `http_proxy` options
    this.http_proxy = this.profile.http_proxy || null

    // Disable Lrc by default
    this.isShowLrc = false

    // Update UI
    template.updateTab('Douban FM')
  }

  /**
  *
  * Init douban.fm command line interface.
  * @return {Promise}
  *
  **/
  init() {
    return fs.existAsync(this.home)
      .then(exist => {
        if (exist)
          return this.createMenu()

        // errors.mkdir_fail
        return mkdirAsync(this.home)
          .then(() => this.createMenu())
      })
  }

  /**
   * [Fetch songs and add them to playlist]
   * @param  {Object}   channel 
   * @param  {Object}   account 
   * @return {Promise}           
   */
  fetch({ channel_id }, account) {
    if (utils.isLocalChannel('local', channel_id)) {
      return utils.locals({
        localPath: this.home,
        historyPath: this.path.history
      })
    }

    const query = {
      kbps: 192,
      channel: channel_id,
    }

    if (account) {
      ;['token', 'user_id', 'expire'].forEach(item =>
        query[item] = account[item])
    }

    return douban.fm.fresh(query)
      .then(({ body }) => {
        if (body.length === 0) 
          return Promise.reject(new Error('No songs fetched'))
        if (!this.player)
          return Promise.reject(new Error('No valid player'))

        body.forEach(this.player.add)

        return Promise.resolve(body)
      })
  }

  /**
  *
  * [Create command line interface menu]
  * [Using term-list-enhanced module]
  * @param {Function} callback [The callback function when all set done]
  *
  **/
  createMenu() {
    // Fetch channels
    // return a Promise
    return douban.fm.channels(list => {
      // Fetch configs, Show user's infomation
      return fs.readJSONAsync(this.path.profile)
        .then(user => {
          var vaildAccount = user && user.account && user.account.user_name
          var account = vaildAccount ? user.account : null

          // Init menu
          this.menu = new termList()
          this.menu.header(template.logo(account))
          this.menu.adds(
            [sdk.mhz.localMhz].concat(!err ? [sdk.mhz.privateMhz].concat(list) : [])
          )

          // Bind keypress events
          this.menu.on('keypress', (key, index) => {
            if (!shorthands[key.name]) 
              return false;

            return this[shorthands[key.name]](this.menu.items[index], account)
          })

          this.menu.on('empty', () => {
            this.menu.stop()
          })

          // Check last played channel,
          // If it existed, play this channel instantly.
          if (user && user.lastChannel) {
            this.play(user.lastChannel, account)
            this.menu.start(user.lastChannel.index)
            return false
          }

          // Start menu at line 2,
          // Which below the logo.
          this.menu.start(1)
        })
    }).catch(err => {
      console.log(errors.turn_to_local_mode)
    })
  }

  /**
   * [Playing songs when everything is ready]
   * @param  {Object} channel 
   * @param  {Object} account 
   * @return {}         
   */
  play(channel, account) {
    var self = this
    var menu = this.menu
    var isVaildAccount = account && account.token
    var privateMhz = isLocalChannel('private', channel.channel_id) && !isVaildAccount

    // Check if this kind of mHz is private
    if (privateMhz)
      return menu.update('header', errors.account_missing)

    // Clear last label
    if (this.status === 'fetching' || this.status === 'downloading')
      return

    if (this.status === 'playing' || this.status === 'error') {
      if (typeof(this.channel) != undefined) 
        menu.clear(this.channel)

      if (this.player) {
        this.player.stop()
        delete this.player
      }
    }

    // Clear label status
    menu.clear('header')
    this.channel = channel.index
    this.status = 'fetching'
    menu.update(channel.index, template.listing())

    try {
      fs.updateJSON(this.path.profile, { lastChannel: channel })
    } catch (err) {};

    // Start fetching songs
    this.fetch(channel.channel_id, account, ({songs, result}) => {
      // Mark a `PRO` label on logo
      if (result && !result.warning) 
        menu.update('header', color.inverse(' PRO '));

      this.status = 'ready';
      this.player = new Player(songs, {
        src: 'url',
        cache: true,
        downloads: this.home,
        http_proxy: this.http_proxy
      });

      this.player.play();
      this.player.on('downloading', onDownloading);
      this.player.on('playing', onPlaying);

      function onDownloading(url) {
        self.status = 'downloading';
        menu.update(channel.index, template.loading());
      }

      function onPlaying(song) {
        var isValidSong = song.title && song.sid;
        self.status = 'playing';
        // Update playing label
        menu.update('header', color.green('>'));
        // Update song infomation
        menu.update(channel.index, template.song(song));
        // Logging songs history
        if (isValidSong) {
          var updates = {};
          updates[song.sid] = song;
          try {
            fs.updateJSON(self.path.history, updates);
          } catch (err) {
            // Errors must be logged in a private place.
          }
        }

        // Print LRC if needed.
        if (self.isShowLrc) {
          if (self.lrc) 
            self.lrc.stop();

          geci.fetch(song, printLrc);

          function printLrc(err, lrc) {
            if (err)
              return menu.update('header', color.grey(errors.lrc_notfound + err.toString()));
            if (!lrc)
              return menu.update('header', color.grey(errors.lrc_notfound));

            self.lrc = geci.print(lrc, (line, extra) => {
              menu.update(channel.index, template.song(song, line));
            });
          }
        }

        // TODO: Still trying after failed two times.
        if (song._id < self.player.list.length - 1)
          return false;

        return self.fetch(channel, account);
      }
    }).catch(err => {
      this.status = 'error'

      return menu.update(
        channel.index, 
        color.red(err.toString())
      )
    })
  }

  /**
  *
  * Play the next song in the playlist
  * @param channel {Object}
  * @param account {Object}
  *
  **/
  next(channel, account) {
    if (!this.player)
      return

    this.player.next((err, song) => {
      if (err) 
        this.menu.update('header', errors.last_song)

      return
    })
  }

  /**
  *
  * [Stop playing]
  * [And show the stopped status on logo.]
  * @param channel {Object}
  * @param account {Object}
  *
  **/
  stop(channel, account) {
    if (!this.player)
      return
    if (this.status === 'stopped')
      return this.play(channel, account)

    var menu = this.menu
    menu.clear('header')
    menu.update('header', template.pause())
    this.status = 'stopped'

    return this.player.stop()
  }

  /**
  *
  * [Add current song to lovelist when pressing `L`]
  * @param channel {Object}
  * @param account {Object}
  *
  **/
  loving(channel, account) {
    if (!this.player.playing)
      return Project.reject(new Error('No song is playing'))
    if (!this.player.playing.sid)
      return this.menu.update('header', errors.love_fail)
    if (!account)
      return this.menu.update('header', errors.account_missing)

    var menu = this.menu
    var song = this.player.playing
    var query = {
      sid: song.sid,
      channel: this.channel,
      user_id: account.user_id,
      expire: account.expire,
      token: account.token
    }

    menu.update('header', '正在加载...')

    douban.fm[ song.like ? 'unrete': 'rate'](query, result => {
      menu.clear('header')

      this.player.playing.like = !song.like

      return menu.update(
        this.channel,
        // keep silence, do not notify
        template.song(this.player.playing, null, true) 
      )
    }).catch(err => {
      menu.clear('header')
      menu.update('header', errors.normal)
    })
  }

  /**
  *
  * [Show lrc when when pressing `R`]
  * @param channel {Object}
  * @param account {Object}
  *
  **/
  showLrc({ channel_id }) {
    if (channel_id == -99)
      return Promise.reject('It is a local channel')

    this.isShowLrc = !!!this.isShowLrc
    this.menu.clear('header')
    this.menu.update('header', this.isShowLrc ? '歌词开启' : '歌词关闭')

    return Promise.resolve(this.isShowLrc)
  }

  /**
  *
  * [Goto the music album page when pressing `G`]
  * @param {Object} channel
  *
  **/
  go({ channel_id }) {
    if (channel_id === -99)
      return Project.reject(new Error('It is a local channel'))
    if (!this.player.playing)
      return Project.reject(new Error('No song is playing'))

    return open(
      utils.album(this.player.playing.album)
    )
  }

  /**
  *
  * [Share the current playing songs to Weibo when pressing `S`]
  * @param channel {Object}
  * @param account {Object}
  *
  **/
  share() {
    if (!this.player.playing)
      return Project.reject(new Error('No song is playing'))

    return open(
      template.share(this.player.playing)
    )
  }

  /**
  *
  * [Quit the FM]
  * [And kill the process when pressing `Q`]
  *
  **/
  quit() {
    this.menu.stop()
    return process.exit()
  }
}
